#!/bin/bash -n

# module: core.sh

shopt -s expand_aliases extglob;

assert() {

    # assert ... matches ...
    [[ $2 = matches ]] && {
        [[ "$1" =~ $3 ]];
        return;
    }

    # assert ... is ...
    [[ $2 = is ]] && {

        [[ $3 = integer ]] && {
            [[ $1 =~ [0-9]+ ]];
            return;
        }

        # assert ... is nothing
        [[ $3 = nothing ]] && {
            [[ -z "$1" ]];
            return;
        }

        # assert ... is value
        [[ $3 = value ]] && {
            [[ -n "$1" ]];
            return;
        }

        # assert ... is declared
        [[ $3 = declared ]] && {
            local buffer;

            assert "$1" is nothing && return 1;

            [[ "$1" =~ \[.+\]$ ]] && {

                local group="${1%[*}";

                local name="`buffer=${1#*[}; echo -n ${buffer%*]}`";

                eval local list=\${!$group[@]};

                [[ "$list" =~ (^|" ")$name(" "|$) ]];

            } || {

                eval local list=\${!$1@};

                [[ "$list" =~ (^|" ")$1(" "|$) ]];

            }

            return;

        } 2>&- 1>/dev/null; # assert ... is declared ends

        # assert ... is shell ...
        [[ $3 = shell ]] && {

            # assert ... is shell file hash
            [[ $4 = file && $5 = hash ]] && {
                hash -t "$1" 2>&- 1>/dev/null;
                return;
            } # assert ... is shell file hash ends

            # assert ... is shell command
            [[ $4 = command ]] && {
                type "$1" 2>&- 1>/dev/null;
                return;
            } # assert ... is shell command ends

            # assert ... is shell builtin|file|alias|keyword|function
            [[  $4 = builtin || $4 = file   || \
                $4 = alias   || $4 = keyword || $4 = function ]] && {
                [[ $(type -t "$1" 2>&-) = $4 ]];
                return;
            } # assert ... is shell builtin|file|alias|keyword|function ends

            return;

        } # assert ... is shell ... ends

        # assert ... is variant ...
        [[ $3 = variant ]] && {

            # assert ... is variant lable
            [[ $4 = lable ]] && {
                [[ "$1" =~ ^[_A-Za-z][_A-Za-z0-9]+$ ]]
                return;
            } # assert ... is variant lable ends
            
            # assert ... is variant object
            [[ $4 = object ]] && {
                declare -pA ${1:-invalid.property};
                return;
            } # assert ... is variant object ends

            # assert ... is variant array
            [[ $4 = array ]] && {
                declare -pa ${1:-invalid.property};
                return;
            } # assert ... is variant array ends

            # assert ... is variant integer
            [[ $4 = integer ]] && {
                declare -pi ${1:-invalid.property};
                return;
            } # assert ... is variant integer ends

            return;

        } 2>&- >/dev/null; # assert ... is variant ... ends

        # assert ... is ... file
        [[ $4 = file ]] && {

            [[ $1 = output && $3 = terminal ]] && {
                [[ -t 1 ]];
                return;
            }

            [[ $1 = input && $3 = terminal ]] && {
                [[ -t 0 ]];
                return;
            }

            [[ $1 = error && $3 = terminal ]] && {
                [[ -t 2 ]];
                return;
            }

            # assert ... is existing file
            [[ $3 = existing ]] && {
                [[ -e "$1" ]];
                return;

            } # assert ... is existing file ends

            # assert ... is regular file
            [[ $3 = regular ]] && {
                [[ -r "$1" ]];
                return;
            } # assert ... is regular file ends

            # assert ... is block file
            [[ $3 = block ]] && {
                [[ -b "$1" ]];
                return;
            } # assert ... is block file ends

            # assert ... is sticky file
            [[ $3 = sticky ]] && {
                [[ -k "$1" ]];
                return;
            } # assert ... is sticky file ends

            # assert ... is readable file
            [[ $3 = readable ]] && {
                [[ -r "$1" ]];
                return;
            } # assert ... is readable file ends

            # assert ... is writeable file
            [[ $3 = writeable ]] && {
                [[ -w "$1" ]];
                return;
            } # assert ... is writeable file ends

            # assert ... is character file
            [[ $3 = character ]] && {
                [[ -c "$1" ]];
                return;
            } # assert ... is character file ends

            # assert ... is empty file
            [[ $3 = empty ]] && {
                [[ ! -s "$1" ]];
                return;
            } # assert ... is empty file ends

            # assert ... is socket file
            [[ $3 = socket ]] && {
                [[ -S "$1" ]];
                return;
            } # assert ... is socket file ends

            # assert ... is executable file
            [[ $3 = executable ]] && {
                [[ ! -x "$1" ]];
                return;
            } # assert ... is executable file ends

            # assert ... is user file
            [[ $3 = user ]] && {
                [[ ! -O "$1" ]];
                return;
            } # assert ... is user file ends

            # assert ... is terminal file descriptor
            [[ $3 = terminal && $5 = descriptor ]] && {
                [[ -t "$1" ]];
                return;
            } # assert ... is terminal file descriptor

            # assert ... is group file
            [[ $3 = group ]] && {
                [[ ! -G "$1" ]];
                return;
            } # assert ... is group file ends

            # assert ... is modified file
            [[ $3 = modified ]] && {
                [[ -N "$1" ]];
                return;
            } # assert ... is modified file ends

            return;
        }

        # assert ... is set ... id file
        [[ $3 = set && $5 = id && $6 = file ]] && {

            # assert ... is set user id file
            [[ $4 = user ]] && {
                [[ -u "$1" ]];
                return;
            } # assert ... is set user id file ends

            [[ $4 = group ]] && {
                [[ -g "$1" ]];
                return;
            }

            return

        } # assert ... is set ... id file ends

        # assert ... is symbolic link
        [[ $3 = symbolic && $4 = link ]] && {
            [[ -L "$1" ]];
            return;
        } # assert ... is symbolic link ends

        # assert ... is file directory
        [[ $3 = file && $4 = directory ]] && {
            [[ -d "$1" ]];
            return;
        } # assert ... is file directory ends

        # assert ... is pipe
        [[ $3 = pipe ]] && {
            [[ -p "$1" ]];
            return;
        }

        [[  $3 = class && $4 = module ]] && {
            assert $1 is shell file && {
                while read -n ${#1}; do
                    [[ $REPLY =~ ^\# ]] && continue;
                    [[ "$REPLY" = $1 || "$REPLY" = "function $1" ]] && return;
                    return;
                done < $(type -p $1);
            } 
            return;
        }

        return;

    } # assert ... is ... ends

}

bash.io.write() {

    local -A color='(
        [yellow]="6" [red]="4" [blue]="1" [white]="7" 
        [green]="2" [black]="0" [magenta]="5" [cyan]="3" 
    )';

    local -i save=0 ref;

    assert format is variant object || {
        local -A format[0]='%s\n';
        local oldformat='';
    } && {
        local oldformat="$(declare -p format)";
    }

    format[currency]="$%'.2f" format[line]="%s\n" format[string]="%s" \
    format[quote]='%q' format[expand]='%b' format[hex]='%x' format[octal]='%o' \
    format[word]='%s ' format[indent]="%*s";

    while [[ $# -gt 0 ]]; do
        [[ "$1" = '--' ]] && {

            shift;
            printf "$format" "$@" 2>&-;
            break;

        } || {

            [[ $1 =~ ^--repeat-string|^-r$ ]] && {
                local temp;
                printf -v temp %*s "$2";
                printf "%s" "${temp// /$3}";
                shift 3;
                continue;
            }

            [[ $1 =~ ^--whitespace|^-w$ ]] && {
                printf %*s $2;
                shift 2;
                continue;
            }

            [[ $1 =~ ^--set|^-S$ ]] && {
                shift;
                let save++;
                continue;
            }

            [[ $1 =~ ^--color$|^-c$ ]] && {
                shift;
                assert output is terminal file && {
                    assert $1 is integer && {
                         tput setf $1;
                    } || {
                        ref="color[$1]"; # ref is declared integer and 
                                          # color[$1] = integer
                        tput setf ${ref};
                    }
                }
                shift;
                continue;
            }

            [[ $1 =~ ^--format$|^-f$ && "$2" = '+=' ]] && {

                format+="$3"

                shift 3;
                continue;

            }

            [[ $1 =~ ^--format$|^-f$ ]] && {

                local class=format[$2];

                assert $class is declared && {
                    format="${!class}";
                } || {
                    format="$2";
                }

                shift 2;

                while [[ "$1" = '+=' ]]; do

                    class=format[$2];

                    assert $class is declared && {
                        format+=${!class};
                    } || {
                        format+="$2";
                    }

                    shift 2;

                done

                continue;

            }

            [[ $1 =~ ^--bold$|^-b$ ]] && {
                shift;
                assert output is terminal file && tput bold;
                continue;
            }

            [[ $1 =~ ^--cursor$|^-C$ ]] && { # location is x, y
                shift;
                assert output is terminal file && tput cup ${2} ${1//,/};
                shift 2;
                continue;
            }

            [[ $1 =~ ^--underline$|^-u$ ]] && {
                shift;
                assert output is terminal file && tput smul;
                continue;
            }

            ( set -- "$1"; printf "$format" "$@" 2>&-; )

        }

        shift;

    done;

    assert output is terminal file && (( ! save )) && {
        tput sgr0;
    }

    . <(echo -En "${oldformat/declare/declare -g}");

}

declare this;

use() {
    this="$1";
}

new() {

    local constructor="$1"; shift;

    assert $constructor is shell function && {
        $constructor init "$@";
        return;
    }

    assert $constructor is class module && {
        source $constructor;
        $constructor init "$@";
        return;
    }

    echo new $constructor: failure: \
    constructor is not a function or class module in the path >&2;

    return 1;

}

bash.report() { # title command

    local RESULT='' title="$1"; shift;

    assert columns variant is integer || {

        {
          assert output is terminal file \
          && local -i columns=$(tput cols) \
          || local -i columns=70;
        }

    }

    .failure() {

        local -i code=$?;

        columns=$columns-9;

        bash.io.write --bold \
            --format "%*s" $((${columns}-${#title})) \
            --format string \
            --color white \
        '[' --color red FAILED --color white -f line ']'

        [[ -n $REPLY ]] && {
            bash.io.write -b -c red '' "${REPLY##$'\n'}" '';
        }

        unset -f .failure .success;

        return $code;

    }

    .success() {

        columns=$columns-5;

        bash.io.write --bold \
            --format "%*s" $((${columns}-${#title})) \
            --format string \
            --color white \
        '[' --color green OK --color white -f line ']'

        [[ -z $REPLY ]] || {
            bash.io.write -b -c yellow '' "${REPLY#$'\n'}" '';
        }
        
        unset -f .failure .success;

    }

    local -i maxwidth=$(($columns-10))-3;

    title="${title:0:$maxwidth}...";
    
    bash.io.write --bold --format '%s' "$title";
    
    REPLY=`eval "$@" 2>&1` && .success || .failure;

}

bash.class.module(){

    # typically called on to install class files using "new" function.

    # new bash.class.module acme.com.module.name \
    # [base=$RESOURCEPATH/object] [source=file]

    [[ "$1" = init ]] && { (

        local -A option;

        local classid=option[classid] source=option[source] base=option[base];

        local path link destination basedir="$HOME/bash-masters/object";

        shift;

        object init option classid="$@";

        basedir="${!base:-$basedir}";
        destination="$basedir/${!classid//.//}.ref";
        path="${destination%/*}";
        link="${basedir%/*}/${!classid}";

        assert "$path" is file directory || mkdir -p "$path";

        # source = destination == undefined!
        # need a better descriptor than a path name

        cat "${!source:--}" > "$destination";

        chmod +x "$destination";

        ln -fs "$destination" "$link";

    )   }


}

bash.configure.objects() {

    local IFS=$'\n' base="${1:-$HOME/bash-masters}";
    local file id link;

    local -i err;

    assert "$base" is file directory && assert "$base" is writeable file && {

        # remove invalid links in the target directory..
        for file in `ls -F $base`; do
            [[ $file =~ @$ ]] && [[ ! -e $base/${file%@} ]] && rm $base/${file%@};
        done;

        IFS=$'\n';

        pushd "$base/object" >/dev/null;

        for file in $(find "$base/object" -type f -name "*.ref"); do

            id=${file/"$base/object/"/}
            id="${id%%.ref*}";
            link="$base/${id//\//.}";
            report=$(ln -fs "$file" "$link" 2>&1) || {
                [[ -d "$link" ]] && {
                    echo $FUNCNAME: \
                    cannot create link: \'${link}\': name is an existing directory;
                } || {
                    echo $FUNCNAME: unknown link error: "$report";
                }
                let err++;
            } >&2;
            chmod +x "$file"

        done

        popd >/dev/null;

    } || {

        echo "$FUNCNAME: '$base' is not a writeable file directory";
        return 1;

    } >&2;

    return $err;

}

function object() {

    assert "$1" matches "^(init|type)$" && {

        # execute unchecked operation defined by this class

        shift; case "${BASH_REMATCH[1]}" in

            init)

                # $1 must also follow standard bash naming conventions.
                assert "$1" is value || {
                    echo $FUNCNAME: init error: identifier is null >&2;
                    return 1;
                }
                
                assert "$1" is variant lable || {
                    echo $FUNCNAME: init error: invalid identifier: "'$1'" >&2;
                    return 1;
                }
                
                this="$1"; shift;

                assert "$this" is variant object || {
                     declare -Ag "$this" || return;
                }

                local argument;

                while (( $# )); do

                    [[ "$1" =~ ^=$ ]] && {
                        object property 0 = "${2}";
                        shift 2; continue;
                    }

                    [[ "$1" =~ ^([^=]+)=?(.*)?$ ]] && {
                        object property ${BASH_REMATCH[1]} = "${BASH_REMATCH[2]}";
                        shift; continue;
                    }
                    
                    [[ "$1" =~ ^=(.*)?$ ]] && {
                        object property 0 = "${BASH_REMATCH[1]}";
                        shift; continue;
                    }

                    echo $FUNCNAME: unrecognized initializer: "'$1'" >&2;
                    
                    return 1;

                done;

                argument=$this[onUnload];
                local eventcode=${!argument};
                assert "$eventcode" is value && {
                    unset $argument;
                    object private onUnload = "$eventcode";
                }

                local registry="_${this}_method";

                assert "${!registry}" \
                matches "^$this | $this " || declare -g $registry+="$this ";
                alias $this="this=$this object property 0";

                object method unload this=$this object unload;

            ;;

            type)

                assert "$this" is value || {
                    echo this is a null value;
                    return 1;
                }
                assert "$this" is variant lable || {
                    echo "'$this' is not a valid identifier";
                    return 2;
                }
                assert "$this" is variant object || {
                    echo $this is not an object;
                    return 3;
                }

                echo $this is an object;
                
            ;;

        esac

        return;

    }

    assert "$1" matches "^(property|properties|methods?|private|unload|onLoad)$" && {

        # execute checked operation defined by this class
        local status=$(object type) || {
            [[ $? -ne 3 ]] && {
               echo $FUNCNAME: error: $status >&2;
               return 1;
            }
        }

        local private="_${this}_";

        shift; case ${BASH_REMATCH[1]} in

            property)

                local lable operator option;

                assert "$1" matches ^private$ && {
                    option='-g'; lable="${!1}${2//./_}"; shift 2;
                } || {
                    option='-Ag'; lable="$this[$1]"; shift;
                }

                [[ $# -ge 1 ]] && {
                    operator=$1; shift;
                    declare $option ${lable}${operator}"$*";
                    return;
                }

                assert "${lable}" is declared && echo -n "${!lable}";

            ;;

            method)

                assert "$this" is variant object || {
                    echo -n $FUNCNAME: error \
                    cannot define method: \'$this\' is not an object >&2;
                    return 1;
                }

                local registry="${private}method" method="$1"; shift;

                method="${this}.${method}";

                assert "${!registry}" \
                matches "^$method | $method " || declare -g $registry+="$method ";
                alias ${method}="$*";

            ;;

            private)

                object property private "$@";

            ;;

            methods)

                assert "$class" is value || {
                    echo $FUNCNAME: methods: error: class is not a value >&2;
                    return 1;
                }

                assert "$this" is variant object || {
                    echo $FUNCNAME: methods: error: "'$this'" is not an object >&2;
                    return 1;
                }

                local method= data= registry="${private}method";

                for method; do
                    data="this=$this $class $method";
                    method="${this}.${method}";
                    assert "${!registry}" \
                    matches "^$method | $method " || declare -g \
                    $registry+="$method ";
                    alias ${method}="${data}";
                done;

            ;;

            properties)

                assert "$this" is variant object || {
                    echo $FUNCNAME: properties: error: \
                    "'$this'" is not an object >&2;
                    return 1;
                }

                local property= data= registry="${private}method";

                for property; do
                    data="this=$this object property $property";
                    property="${this}.${property}";
                    assert "${!registry}" \
                    matches "^$property | $property " || declare -g \
                    $registry+="$property ";
                    alias ${property}="${data}";
                done;

            ;;

            onLoad)
                local event=${this}[onLoad];
                local eventcode=${!event};
                unset $event;
                eval ${eventcode:-false};
            ;;

            unload)

                local property="${private}onUnload" data;
                eval ${!property:-false};
                property="${private}method"; unalias ${!property};
                data='${!'"${private}"'@}'; eval unset $this $data;
                this='';

                true; # unload always returns true even if it fails

            ;;

        esac

        return;

    }

    echo $FUNCNAME: error: unhandled operation: \'$1\' >&2;
    false;

}

alias script:='<<EOF' document='<<"EOF"';

hash cat sed sed awk grep head tail wc tty tput;

